package org.breathe.rdb;

import lombok.extern.slf4j.Slf4j;
import org.breathe.datastruct.RedisBytes;
import org.breathe.datastruct.impl.*;
import org.breathe.internal.Dict;
import org.breathe.internal.Sds;
import org.breathe.server.core.RedisCore;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author: breathe
 * @createTime: 2025-06-08
 */
@Slf4j
public class RdbUtils {
    public static void writeRdbHeader(DataOutputStream dos) throws IOException {
        dos.writeBytes("REDIS0009");

    }

    public static void writeRdbTail(DataOutputStream dos) throws IOException {
        dos.writeByte(RdbConstants.RDB_OPCODE_EOF);
        //TODO 添加校验和机制
        dos.writeLong(0);
    }
    private static long readLength(DataInputStream dis) throws IOException {
        int firstByte = dis.readByte()&0xFF;
        int type = (firstByte & 0xC0)>>6;
        switch(type){
            case 0:
                return firstByte & 0x3F;
            case 1:
                int secondByte = dis.readByte() & 0xFF;
                return ((firstByte & 0x3F) << 8) | secondByte;
            case 2:
                return dis.readInt();
            default:
                throw new RuntimeException("read length error");
        }
    }
    private static byte[] readString(DataInputStream dis) throws IOException {
        long length  = readLength(dis);
        if(length < 0){
            throw new RuntimeException("read string length error");
        }
        int len = (int) length;
        byte[] bytes = new byte[len];
        dis.readFully(bytes);
        return bytes;
    }
    public static void writeSelectDB(DataOutputStream dos, int databaseId) throws IOException {
        dos.writeByte(RdbConstants.RDB_OPCODE_SELECT_DB);
        writeLength(dos, databaseId);
    }

    private static void writeLength(DataOutputStream dos, int length) throws IOException {
        if (length < 0x40) {
            dos.writeByte(length);
        } else if (length < 0x4000) {
            dos.writeByte(length | 0x4000);
        } else {
            dos.writeByte(0x80);
            dos.writeByte(length);
        }
    }

    public static void saveString(DataOutputStream dos, RedisBytes key, RedisString value) throws IOException {
        dos.writeByte(RdbConstants.STRING_TYPE);
        writeString(dos, key.getBytes());
        writeString(dos, value.getValue().getBytes());
    }
    public static void writeString(DataOutputStream dos, byte[] bytes) throws IOException {
        writeLength(dos, bytes.length);
        dos.write(bytes);
    }

    public static void saveList(DataOutputStream dos, RedisBytes key, RedisList value) throws IOException {
        dos.writeByte(RdbConstants.LIST_TYPE);
        writeString(dos, key.getBytes());
        writeLength(dos, value.size());
        for (RedisBytes bytes : value.getAll()) {
            writeString(dos, bytes.getBytes());
        }
    }

    public static void saveSet(DataOutputStream dos, RedisBytes key, RedisSet value) throws IOException {
        dos.writeByte(RdbConstants.SET_TYPE);
        writeString(dos, key.getBytes());
        writeLength(dos, value.size());
        for (RedisBytes bytes : value.getAll()) {
            writeString(dos, bytes.getBytes());
        }
        log.info("save set: {}", key);
    }

    public static void saveHash(DataOutputStream dos, RedisBytes key, RedisHash value) throws IOException {
        dos.writeByte(RdbConstants.HASH_TYPE);
        writeString(dos, key.getBytes());
        Dict<RedisBytes, RedisBytes> hash = value.getHash();
        writeLength(dos, hash.size());
        for(Map.Entry<RedisBytes, Object> entry : hash.entrySet()){
            writeString(dos,(entry.getKey()).getBytes());
            writeString(dos,((RedisBytes)entry.getValue()).getBytes());
        }
        log.info("save hash: {}", key);
    }

    public static void saveZSet(DataOutputStream dos, RedisBytes key, RedisZSet value) throws IOException {
        dos.writeByte(RdbConstants.ZSET_TYPE);
        writeString(dos, key.getBytes());
        int size = value.size();
        writeLength(dos, size);
        @SuppressWarnings("unchecked")
        Iterable<? extends Map.Entry<Double, Object>> entries = value.getAll();
        for(Map.Entry<Double, Object> entry : entries){
            writeString(dos,String.valueOf(entry.getKey()).getBytes());
            writeString(dos,((RedisBytes)entry.getValue()).getBytes());
        }
        log.info("save zset: {}", key);
    }

    public static boolean checkRdbHeader(DataInputStream dis) throws IOException {
        byte[] header = new byte[9];
        int read = dis.read(header);
        return read == 9 && new String(header).equals("REDIS0009");
    }

    public static void loadString(DataInputStream dis, RedisCore redisCore, int currentDbIndex) throws IOException {
        RedisBytes key = new RedisBytes(RdbUtils.readString(dis));
        RedisBytes value = new RedisBytes(RdbUtils.readString(dis));
        RedisString redisString = new RedisString(new Sds(value.getBytes()));
        redisCore.selectDB(currentDbIndex);
        redisCore.put(key, redisString);
        log.info("load entry to db{}: {} -> {}", currentDbIndex, key.getString(), value.getString());
    }

    public static void loadList(DataInputStream dis, RedisCore redisCore, int currentDbIndex) throws IOException {
        RedisBytes key = new RedisBytes(RdbUtils.readString(dis));
        long size = RdbUtils.readLength(dis);
        RedisList redisList = new RedisList();
        for (int i = 0; i < size; i ++) {
            redisList.lPush(new RedisBytes(RdbUtils.readString(dis)));
        }
        redisCore.selectDB(currentDbIndex);
        redisCore.put(key, redisList);
        log.info("load list to db{}: {} -> {}", currentDbIndex, key.getString(), redisList.lRange(0, (int)(size - 1)));
    }

    public static void loadSet(DataInputStream dis, RedisCore redisCore, int currentDbIndex) throws IOException {
        RedisBytes key = new RedisBytes(RdbUtils.readString(dis));
        long size = RdbUtils.readLength(dis);
        RedisSet redisSet = new RedisSet();
        List<RedisBytes> temp = new ArrayList<>();
        for (int i = 0; i < size; i ++) {
            temp.add(new RedisBytes(readString(dis)));
        }
        redisSet.add(temp);
        redisCore.selectDB(currentDbIndex);
        redisCore.put(key, redisSet);
        log.info("load set to db{}: {} -> {}", currentDbIndex, key.getString(), redisSet.getAll());
    }

    public static void loadHash(DataInputStream dis, RedisCore redisCore, int currentDbIndex) throws IOException {
        RedisBytes key = new RedisBytes(RdbUtils.readString(dis));
        long size = RdbUtils.readLength(dis);
        RedisHash redisHash = new RedisHash();
        for (int i = 0; i < size; i ++) {
            RedisBytes field = new RedisBytes(RdbUtils.readString(dis));
            RedisBytes value = new RedisBytes(RdbUtils.readString(dis));
            redisHash.put(field, value);
        }
        redisCore.selectDB(currentDbIndex);
        redisCore.put(key, redisHash);
        log.info("load hash table to db{}: {} -> {}", currentDbIndex, key.getString(), redisHash.getHash());
    }

    public static void loadZSet(DataInputStream dis, RedisCore redisCore, int currentDbIndex) throws IOException {
        RedisBytes key = new RedisBytes(RdbUtils.readString(dis));
        long size = RdbUtils.readLength(dis);
        RedisZSet redisZSet = new RedisZSet();
        for (int i = 0; i < size; i ++) {
            RedisBytes score = new RedisBytes(RdbUtils.readString(dis));
            RedisBytes member = new RedisBytes(RdbUtils.readString(dis));
            redisZSet.add(Double.parseDouble(new String(score.getBytes())), member);
        }
        redisCore.selectDB(currentDbIndex);
        redisCore.put(key, redisZSet);
        log.info("load zset to db{}: {} -> {}", currentDbIndex, key.getString(), redisZSet.getAll());
    }
}
